Čeština

Objevte TypeScript branded types, techniku pro nominální typování ve strukturálním systému. Zvyšte typovou bezpečnost a srozumitelnost kódu.

TypeScript Branded Types: Nominální typování ve strukturálním systému

Strukturální typový systém TypeScriptu nabízí flexibilitu, ale někdy může vést k neočekávanému chování. Branded types poskytují způsob, jak vynutit nominální typování, čímž zvyšují typovou bezpečnost a srozumitelnost kódu. Tento článek podrobně prozkoumává branded types a poskytuje praktické příklady a osvědčené postupy pro jejich implementaci.

Porozumění strukturálnímu a nominálnímu typování

Než se ponoříme do branded types, ujasněme si rozdíl mezi strukturálním a nominálním typováním.

Strukturální typování (Duck Typing)

Ve strukturálním typovém systému jsou dva typy považovány za kompatibilní, pokud mají stejnou strukturu (tj. stejné vlastnosti se stejnými typy). TypeScript používá strukturální typování. Zvažte tento příklad:


interface Point {
  x: number;
  y: number;
}

interface Vector {
  x: number;
  y: number;
}

const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // V TypeScriptu platné

console.log(vector.x); // Výstup: 10

I když jsou Point a Vector deklarovány jako odlišné typy, TypeScript umožňuje přiřadit objekt typu Point proměnné typu Vector, protože sdílejí stejnou strukturu. To může být pohodlné, ale může to také vést k chybám, pokud potřebujete rozlišovat mezi logicky odlišnými typy, které mají náhodou stejný tvar. Například souřadnice zeměpisné šířky/délky, které by se mohly náhodně shodovat se souřadnicemi pixelů na obrazovce.

Nominální typování

V nominálním typovém systému jsou typy považovány za kompatibilní pouze tehdy, mají-li stejné jméno. I když mají dva typy stejnou strukturu, jsou považovány za odlišné, pokud mají různá jména. Jazyky jako Java a C# používají nominální typování.

Potřeba Branded Types

Strukturální typování TypeScriptu může být problematické, když potřebujete zajistit, aby hodnota patřila ke konkrétnímu typu bez ohledu na její strukturu. Zvažte například reprezentaci měn. Můžete mít různé typy pro USD a EUR, ale oba by mohly být reprezentovány jako čísla. Bez mechanismu pro jejich rozlišení byste mohli omylem provádět operace se špatnou měnou.

Branded types tento problém řeší tím, že vám umožňují vytvářet odlišné typy, které jsou strukturálně podobné, ale typovým systémem jsou považovány za různé. To zvyšuje typovou bezpečnost a předchází chybám, které by jinak mohly proklouznout.

Implementace Branded Types v TypeScriptu

Branded types jsou implementovány pomocí průnikových typů (intersection types) a jedinečného symbolu nebo řetězcového literálu. Myšlenka spočívá v přidání "značky" (brand) k typu, která jej odliší od ostatních typů se stejnou strukturou.

Použití symbolů (doporučeno)

Použití symbolů pro branding je obecně preferováno, protože symboly jsou zaručeně jedinečné.


const USD = Symbol('USD');
type USD = number & { readonly [USD]: unique symbol };

const EUR = Symbol('EUR');
type EUR = number & { readonly [EUR]: unique symbol };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Celkem USD:", totalUSD);

// Odkomentování následujícího řádku způsobí typovou chybu
// const invalidOperation = addUSD(usd1, eur1);

V tomto příkladu jsou USD a EUR branded types založené na typu number. unique symbol zajišťuje, že tyto typy jsou odlišné. Funkce createUSD a createEUR se používají k vytváření hodnot těchto typů a funkce addUSD přijímá pouze hodnoty typu USD. Pokus o přičtení hodnoty EUR k hodnotě USD povede k typové chybě.

Použití řetězcových literálů

Pro branding můžete také použít řetězcové literály, i když tento přístup je méně robustní než použití symbolů, protože řetězcové literály nejsou zaručeně jedinečné.


type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Celkem USD:", totalUSD);

// Odkomentování následujícího řádku způsobí typovou chybu
// const invalidOperation = addUSD(usd1, eur1);

Tento příklad dosahuje stejného výsledku jako předchozí, ale používá řetězcové literály místo symbolů. I když je to jednodušší, je důležité zajistit, aby řetězcové literály použité pro branding byly v rámci vaší kódové báze jedinečné.

Praktické příklady a případy použití

Branded types lze aplikovat na různé scénáře, kde potřebujete vynutit typovou bezpečnost nad rámec strukturální kompatibility.

Identifikátory (ID)

Zvažte systém s různými typy ID, jako jsou UserID, ProductID a OrderID. Všechny tyto identifikátory mohou být reprezentovány jako čísla nebo řetězce, ale chcete zabránit náhodnému smíchání různých typů ID.


const UserIDBrand = Symbol('UserID');
type UserID = string & { readonly [UserIDBrand]: unique symbol };

const ProductIDBrand = Symbol('ProductID');
type ProductID = string & { readonly [ProductIDBrand]: unique symbol };

function getUser(id: UserID): { name: string } {
  // ... načtení dat uživatele
  return { name: "Alice" };
}

function getProduct(id: ProductID): { name: string, price: number } {
  // ... načtení dat produktu
  return { name: "Example Product", price: 25 };
}

function createUserID(id: string): UserID {
  return id as UserID;
}

function createProductID(id: string): ProductID {
  return id as ProductID;
}

const userID = createUserID('user123');
const productID = createProductID('product456');

const user = getUser(userID);
const product = getProduct(productID);

console.log("Uživatel:", user);
console.log("Produkt:", product);

// Odkomentování následujícího řádku způsobí typovou chybu
// const invalidCall = getUser(productID);

Tento příklad ukazuje, jak mohou branded types zabránit předání ProductID funkci, která očekává UserID, čímž se zvyšuje typová bezpečnost.

Doménově specifické hodnoty

Branded types mohou být také užitečné pro reprezentaci doménově specifických hodnot s omezeními. Můžete mít například typ pro procenta, která by měla být vždy mezi 0 a 100.


const PercentageBrand = Symbol('Percentage');
type Percentage = number & { readonly [PercentageBrand]: unique symbol };

function createPercentage(value: number): Percentage {
  if (value < 0 || value > 100) {
    throw new Error('Procento musí být mezi 0 a 100');
  }
  return value as Percentage;
}

function applyDiscount(price: number, discount: Percentage): number {
  return price * (1 - discount / 100);
}

try {
  const discount = createPercentage(20);
  const discountedPrice = applyDiscount(100, discount);
  console.log("Zlevněná cena:", discountedPrice);

  // Odkomentování následujícího řádku způsobí chybu za běhu programu
  // const invalidPercentage = createPercentage(120);
} catch (error) {
  console.error(error);
}

Tento příklad ukazuje, jak vynutit omezení na hodnotu branded typu za běhu programu. Zatímco typový systém nemůže zaručit, že hodnota Percentage je vždy mezi 0 a 100, funkce createPercentage může toto omezení vynutit za běhu. Můžete také použít knihovny jako io-ts k vynucení validace branded typů za běhu.

Reprezentace data a času

Práce s daty a časy může být ošidná kvůli různým formátům a časovým pásmům. Branded types mohou pomoci rozlišit mezi různými reprezentacemi data a času.


const UTCDateBrand = Symbol('UTCDate');
type UTCDate = string & { readonly [UTCDateBrand]: unique symbol };

const LocalDateBrand = Symbol('LocalDate');
type LocalDate = string & { readonly [LocalDateBrand]: unique symbol };

function createUTCDate(dateString: string): UTCDate {
  // Ověřit, že řetězec data je ve formátu UTC (např. ISO 8601 s Z)
  if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
    throw new Error('Neplatný formát data UTC');
  }
  return dateString as UTCDate;
}

function createLocalDate(dateString: string): LocalDate {
  // Ověřit, že řetězec data je v lokálním formátu (např. RRRR-MM-DD)
  if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
    throw new Error('Neplatný formát lokálního data');
  }
  return dateString as LocalDate;
}

function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
  // Provede konverzi časového pásma
  const date = new Date(utcDate);
  const localDateString = date.toLocaleDateString();
  return createLocalDate(localDateString);
}

try {
  const utcDate = createUTCDate('2024-01-20T10:00:00.000Z');
  const localDate = convertUTCDateToLocalDate(utcDate);
  console.log("Datum UTC:", utcDate);
  console.log("Lokální datum:", localDate);
} catch (error) {
  console.error(error);
}

Tento příklad rozlišuje mezi daty UTC a lokálními daty, čímž zajišťuje, že v různých částech vaší aplikace pracujete se správnou reprezentací data a času. Validace za běhu zajišťuje, že těmto typům mohou být přiřazeny pouze správně naformátované řetězce data.

Osvědčené postupy pro používání Branded Types

Pro efektivní používání branded types v TypeScriptu zvažte následující osvědčené postupy:

Výhody Branded Types

Nevýhody Branded Types

Alternativy k Branded Types

Ačkoli jsou branded types mocnou technikou pro dosažení nominálního typování v TypeScriptu, existují alternativní přístupy, které můžete zvážit.

Neprůhledné typy (Opaque Types)

Neprůhledné typy jsou podobné branded typům, ale poskytují explicitnější způsob, jak skrýt podkladový typ. TypeScript nemá vestavěnou podporu pro neprůhledné typy, ale můžete je simulovat pomocí modulů a soukromých symbolů.

Třídy

Použití tříd může poskytnout objektově orientovanější přístup k definování odlišných typů. Ačkoli jsou třídy v TypeScriptu typovány strukturálně, nabízejí jasnější oddělení zodpovědností a lze je použít k vynucení omezení prostřednictvím metod.

Knihovny jako `io-ts` nebo `zod`

Tyto knihovny poskytují sofistikovanou validaci typů za běhu a mohou být kombinovány s branded typy k zajištění bezpečnosti jak při kompilaci, tak za běhu.

Závěr

TypeScript branded types jsou cenným nástrojem pro zvýšení typové bezpečnosti a srozumitelnosti kódu ve strukturálním typovém systému. Přidáním "značky" k typu můžete vynutit nominální typování a zabránit náhodnému smíchání strukturálně podobných, ale logicky odlišných typů. Ačkoli branded types přinášejí určitou složitost a režii, výhody zlepšené typové bezpečnosti a udržovatelnosti kódu často převažují nad nevýhodami. Zvažte použití branded typů ve scénářích, kde potřebujete zajistit, aby hodnota patřila ke konkrétnímu typu bez ohledu na její strukturu.

Porozuměním principům strukturálního a nominálního typování a uplatněním osvědčených postupů uvedených v tomto článku můžete efektivně využít branded types k psaní robustnějšího a udržovatelnějšího kódu v TypeScriptu. Od reprezentace měn a ID až po vynucování doménově specifických omezení poskytují branded types flexibilní a mocný mechanismus pro zvýšení typové bezpečnosti ve vašich projektech.

Při práci s TypeScriptem prozkoumejte různé techniky a knihovny dostupné pro validaci a vynucování typů. Zvažte použití branded typů ve spojení s knihovnami pro validaci za běhu, jako jsou io-ts nebo zod, abyste dosáhli komplexního přístupu k typové bezpečnosti.